﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ModbusCase.Lib
{
    /// <summary>
    /// modbus RTU 通讯库 (测试未经优化的封装)
    /// </summary>
    public class ModbusRTU
    {
        private SerialPort _serialPort;
        private byte _slaveId;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="serialPort">串口</param>
        /// <param name="slaveId">从站地址</param>
        public ModbusRTU(SerialPort serialPort, byte slaveId)
        {
            _serialPort = serialPort;
            _slaveId = slaveId;
        }

        /// <summary>
        /// 功能码0x01：读取输出线圈状态
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="count">读取数量</param>
        /// <returns>bool数组</returns>
        public bool[] ReadCoils(ushort startAddress, ushort count)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.分析返回结果。返回bool[]
            // 3.1 校验
            // 3.2 如果通过,取出数据并转换为bool[]

            #region 1.发送请求
            // 起始地址
            byte startAddressHighByte = (byte)(startAddress / 256);
            byte startAddressLowByte = (byte)(startAddress % 256);

            // 数量
            byte countHighByte = (byte)(count >> 8);
            byte countLowByte = (byte)(count & 0XFF);

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x01,     // 功能码
                startAddressHighByte, // 起始地址高字节
                startAddressLowByte, // 起始地址低字节
                countHighByte, // 读取数量高字节
                countLowByte, // 读取数量低字节
            ];

            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的个数/8(返回的字节数) + 5(固定字节)
            int readCount = (int)Math.Ceiling(count * 1.0 / 8) + 5; // 读取字节的个数
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            #endregion

            #region 3.分析返回结果。返回bool[]
            // 报文接收异常情况是将功能码的最高位设置成1了，所以功能码16进制是至少大于0x80，这里01功能码是0x81
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }


            // 3.1 校验
            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }


            // 3.2 如果通过,取出数据并转换为bool[]
            List<byte> dataBytes = readBytes.GetRange(3, readCount - 5);
            // 假设返回：05 00
            // 05= 0000 0101 
            // 00= 0000 0000
            bool[] tempBool = new bool[dataBytes.Count * 8]; // 临时变量
            for (int i = 0; i < dataBytes.Count; i++)
            {
                for (int j = 0; j < 8; j++)
                {
                    // i=0;j=0;
                    //  0000 0101
                    //& 0000 0001
                    //------------
                    //  0000 0001
                    // tempBool[0] = true;

                    // i=0;j=1;
                    //  0000 0101
                    //& 0000 0010
                    //------------
                    //  0000 0000
                    // tempBool[1] = false;

                    // i=0;j=2;
                    //  0000 0101
                    //& 0000 0100
                    //------------
                    //  0000 0100
                    // tempBool[2] = true;
                    tempBool[i * 8 + j] = (dataBytes[i] & (1 << j)) != 0;  // 通过位运算判断每一位是否为1
                }
            }
            #endregion

            bool[] result = new bool[count]; // 最终结果
            Array.Copy(tempBool, result, count);

            return result;
        }


        /// <summary>
        /// 功能码0x02：读取输入(离散)线圈状态
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="count">读取数量</param>
        /// <returns>bool数组</returns>
        public bool[] ReadDiscreteInput(ushort startAddress, ushort count)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.分析返回结果。返回bool[]
            // 3.1 校验
            // 3.2 如果通过,取出数据并转换为bool[]

            #region 1.发送请求
            // 起始地址
            byte startAddressHighByte = (byte)(startAddress / 256);
            byte startAddressLowByte = (byte)(startAddress % 256);

            // 数量
            byte countHighByte = (byte)(count >> 8);
            byte countLowByte = (byte)(count & 0XFF);

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x02,     // 功能码
                startAddressHighByte, // 起始地址高字节
                startAddressLowByte, // 起始地址低字节
                countHighByte, // 读取数量高字节
                countLowByte, // 读取数量低字节
            ];

            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的个数/8(返回的字节数) + 5(固定字节)
            int readCount = (int)Math.Ceiling(count * 1.0 / 8) + 5; // 读取字节的个数
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            #endregion

            #region 3.分析返回结果。返回bool[]
            // 报文接收异常情况是将功能码的最高位设置成1了，所以功能码16进制是至少大于0x80，这里01功能码是0x81
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }


            // 3.1 校验
            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }


            // 3.2 如果通过,取出数据并转换为bool[]
            List<byte> dataBytes = readBytes.GetRange(3, readCount - 5);
            // 假设返回：05 00
            // 05= 0000 0101 
            // 00= 0000 0000
            bool[] tempBool = new bool[dataBytes.Count * 8]; // 临时变量
            for (int i = 0; i < dataBytes.Count; i++)
            {
                for (int j = 0; j < 8; j++)
                {
                    // i=0;j=0;
                    //  0000 0101
                    //& 0000 0001
                    //------------
                    //  0000 0001
                    // tempBool[0] = true;

                    // i=0;j=1;
                    //  0000 0101
                    //& 0000 0010
                    //------------
                    //  0000 0000
                    // tempBool[1] = false;

                    // i=0;j=2;
                    //  0000 0101
                    //& 0000 0100
                    //------------
                    //  0000 0100
                    // tempBool[2] = true;
                    tempBool[i * 8 + j] = (dataBytes[i] & (1 << j)) != 0;  // 通过位运算判断每一位是否为1
                }
            }
            #endregion

            bool[] result = new bool[count]; // 最终结果
            Array.Copy(tempBool, result, count);

            return result;
        }


        /// <summary>
        /// 功能码0x03：读取保持寄存器(16位)
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="count">要读的数字个数</param>
        /// <returns>数字数组</returns>
        public short[] ReadHoldingReg16(ushort startAddress, ushort count)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.分析返回结果。返回short[]
            // 3.1 校验
            // 3.2 如果通过,取出数据并转换为short[]

            #region 1.发送请求
            // 起始地址
            byte startAddressHighByte = (byte)(startAddress / 256);
            byte startAddressLowByte = (byte)(startAddress % 256);

            // 数量(寄存器的个数)
            byte countRegsHighByte = (byte)(count >> 8);
            byte countRegsLowByte = (byte)(count & 0XFF);

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x03,     // 功能码
                startAddressHighByte, // 起始地址高字节
                startAddressLowByte, // 起始地址低字节
                countRegsHighByte, // 读取数量高字节
                countRegsLowByte, // 读取数量低字节
            ];

            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 2(一个寄存器2个字节) + 5(固定字节)
            int readCount = count * 2 + 5; // 读取字节的个数
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            #endregion

            #region 3.分析返回结果。返回short[]
            // 报文接收异常情况是将功能码的最高位设置成1了，所以功能码16进制是至少大于0x80，这里01功能码是0x81
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }


            // 3.1 校验
            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }


            // 3.2 如果通过,取出数据并转换为short[]
            List<byte> dataBytes = readBytes.GetRange(3, readCount - 5);

            short[] result = new short[count];
            for (int i = 0; i < count; i++)
            {
                //BitConverter 只支持小端
                List<byte> curBytes = dataBytes.GetRange(i * 2, 2);
                // 第一种
                //curBytes.Reverse();
                //result[i] = BitConverter.ToInt16(curBytes.ToArray());
                // 第二种
                result[i] = BitConverter.ToInt16([curBytes[1], curBytes[0]]);

            }

            #endregion

            return result;
        }

        /// <summary>
        /// 功能码0x03：读取保持寄存器(32位)
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="count">要读的数字个数</param>
        /// <param name="endian">字节序,大小端模式,默认大端</param>
        /// <returns>数字数组</returns>
        public int[] ReadHoldingReg32(ushort startAddress, ushort count, Endian endian = Endian.BigEndian)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.分析返回结果。返回int[]
            // 3.1 校验
            // 3.2 如果通过,取出数据并转换为int[]

            #region 1.发送请求
            // 起始地址
            byte startAddressHighByte = (byte)(startAddress / 256);
            byte startAddressLowByte = (byte)(startAddress % 256);

            // 数量(寄存器的数量)
            int countRegs = (ushort)(count * 2); // 32位数字要占2个寄存器，所以这里的count要乘以2(1个寄存器为2个字节16位)
            byte countHighByte = (byte)(countRegs >> 8);
            byte countLowByte = (byte)(countRegs & 0XFF);

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x03,     // 功能码
                startAddressHighByte, // 起始地址高字节
                startAddressLowByte, // 起始地址低字节
                countHighByte, // 读取数量高字节
                countLowByte, // 读取数量低字节
            ];

            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 4(一个寄存器2个字节,32位数字占4个字节) + 5(固定字节)
            int readCount = count * 4 + 5; // 读取字节的个数
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            #endregion

            #region 3.分析返回结果。返回int[]
            // 报文接收异常情况是将功能码的最高位设置成1了，所以功能码16进制是至少大于0x80，这里01功能码是0x81
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }


            // 3.1 校验
            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }

            // 3.2 如果通过,取出数据并转换为int[]
            List<byte> dataBytes = readBytes.GetRange(3, readCount - 5);

            int[] result = new int[count];
            for (int i = 0; i < count; i++)
            {
                List<byte> curBytes = dataBytes.GetRange(i * 4, 4);
                if (endian == Endian.BigEndian)  // 如果是大端则反转字节顺序
                {
                    curBytes.Reverse();
                }
                //BitConverter 只支持小端
                result[i] = BitConverter.ToInt32(curBytes.ToArray());
            }

            #endregion

            return result;
        }


        /// <summary>
        /// 功能码0x03：读取保持寄存器(32位 float)
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="count">要读的数字个数</param>
        /// <param name="endian">字节序,大小端模式,默认大端</param>
        /// <returns>数字数组</returns>
        public float[] ReadHoldingReg32Float(ushort startAddress, ushort count, Endian endian = Endian.BigEndian)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.分析返回结果。返回float[]
            // 3.1 校验
            // 3.2 如果通过,取出数据并转换为float[]

            #region 1.发送请求
            // 起始地址
            byte startAddressHighByte = (byte)(startAddress / 256);
            byte startAddressLowByte = (byte)(startAddress % 256);

            // 数量(寄存器的数量)
            int countRegs = (ushort)(count * 2); // 32位数字要占2个寄存器，所以这里的count要乘以2(1个寄存器为2个字节16位)
            byte countHighByte = (byte)(countRegs >> 8);
            byte countLowByte = (byte)(countRegs & 0XFF);

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x03,     // 功能码
                startAddressHighByte, // 起始地址高字节
                startAddressLowByte, // 起始地址低字节
                countHighByte, // 读取数量高字节
                countLowByte, // 读取数量低字节
            ];

            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 4(一个寄存器2个字节,32位数字占4个字节) + 5(固定字节)
            int readCount = count * 4 + 5; // 读取字节的个数
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            #endregion

            #region 3.分析返回结果。返回float[]
            // 报文接收异常情况是将功能码的最高位设置成1了，所以功能码16进制是至少大于0x80，这里01功能码是0x81
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }


            // 3.1 校验
            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }

            // 3.2 如果通过,取出数据并转换为int[]
            List<byte> dataBytes = readBytes.GetRange(3, readCount - 5);

            float[] result = new float[count];
            for (int i = 0; i < count; i++)
            {
                List<byte> curBytes = dataBytes.GetRange(i * 4, 4);
                if (endian == Endian.BigEndian)  // 如果是大端则反转字节顺序
                {
                    curBytes.Reverse();
                }
                //BitConverter 只支持小端
                result[i] = BitConverter.ToSingle(curBytes.ToArray());
            }

            #endregion

            return result;
        }

        #region 写单线圈 功能码0x05
        /// <summary>
        /// 写单线圈 (单个线圈的状态) 功能码0x05
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="value">写的值</param>
        public void WriteSingleCoil(ushort startAddress, bool value)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.校验

            #region 1.发送请求
            // 起始地址
            byte[] startBytes = BitConverter.GetBytes(startAddress);

            byte[] writeValueBytes = value ? [0xFF, 0x00] : [0x00, 0x00]; // 写入的值bytes,如果是true则写入0xFF00,如果是false则写入0x0000


            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x05,     // 功能码
                startBytes[1], // 起始地址
                startBytes[0], // 起始地址
            ];

            sendBytes.AddRange(writeValueBytes); // 添加写入的值

            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 4(一个寄存器2个字节,32位数字占4个字节) + 5(固定字节)
            int readCount = 8; // 读取字节的个数(因为写单线圈功能码0x05返回的报文是8个字节，即从站地址 + 功能码 + 起始地址 + 写入的值 + CRC校验)
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            #endregion

            #region 3.校验
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }

            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }
            #endregion

        }
        #endregion

        #region 写多线圈 功能码0x0F
        /// <summary>
        /// 写多线圈  功能码0x0F
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="values">写多个值</param>
        public void WriteMultipleCoil(ushort startAddress, bool[] values)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.校验
            #region 1.发送请求
            // 起始地址
            byte[] startBytes = BitConverter.GetBytes(startAddress);

            // 写入值的个数
            byte[] valueCountBytes = BitConverter.GetBytes(values.Length);

            // 写入的字节数
            //byte byteCout = (byte)Math.Ceiling(values.Length * 1.0 / 8);
            //byte byteCout = (byte)Math.Ceiling(values.Length / 8.0);
            byte byteCout = (byte)Math.Ceiling(values.Length / 8D);


            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x0F,     // 功能码
                startBytes[1], // 起始地址
                startBytes[0], // 起始地址
                valueCountBytes[1], // 写入的值个数
                valueCountBytes[0],  // 写入的值个数
                byteCout, // 写入的字节数
            ];


            #region c# BitArray的实现
            //// 将bool[]转换为byte[]
            //// 从右向左填充字节(因为Modbus协议是从右向左填充的)
            ////[true, false, true, true]
            //// 0000 1101 <= 从右向左填充  0x0D
            ////[true,false,true,true,true]
            //// 0001 1101

            //BitArray bitArray = new BitArray(values);
            //byte[] writeBytes = new byte[byteCout];
            //bitArray.CopyTo(writeBytes, 0);
            #endregion

            #region 位运算实现
            //// 将bool[]转换为byte[]
            //// 从右向左填充字节(因为Modbus协议是从右向左填充的)
            ////[true, false, true, true]
            //// 0000 1101 <= 从右向左填充  0x0D
            ////[true,false,true,true,true]
            //// 0001 1101
            byte[] writeBytes = new byte[byteCout];
            for (int i = 0; i < values.Length; i++)
            {
                // values[i] 判断当前线圈是否为 true（即是否要设置为 1）。
                // 如果为 true，说明这个 bit 要设为 1。
                // 如果为 false，不需要处理，因为数组初始化时默认是 0。
                if (values[i])
                {
                    // 确定当前 bit 应该写入哪个字节中。  索引 0~7 → 字节索引 0、索引 8~15 → 字节索引 1、索引 16~23 → 字节索引 2
                    int byteIndex = i / 8;
                    // 用 i % 8 得到当前 bit 在字节中的位置。 索引 0 → bit0、索引 1 → bit1、索引 7 → bit7....索引 8 → bit0（属于下一个字节）
                    int bitIndex = i % 8;
                    // 将当前 bit 设置为 1，并写入到对应的字节中。
                    /**
                     * (1 << bitIndex)：左移操作，构造一个只有对应 bit 为 1 的掩码。
                     *  例如：bitIndex = 3 → 1 << 3 = 8 → 二进制 0000 1000
                     *   =：按位或赋值，将当前字节中对应的 bit 设置为 1。
                     *  例如：当前字节是 0000 0000，与 0000 1000 按位或后变为 0000 1000
                     *   (byte)：确保结果是 byte 类型（防止溢出）
                     */
                    writeBytes[byteIndex] |= (byte)(1 << bitIndex);

                    //补充,如果你希望从左到右填充 bit（即索引 0 对应 bit7），可以修改为：
                    /**
                     * 这样：
                     *   索引 0 → bit7
                     *   索引 1 → bit6
                     *   ...
                     *   索引 7 → bit0
                     *   这在某些特定协议中可能会用到。
                     */
                    //writeBytes[byteIndex] |= (byte)(1 << (7 - (i % 8)));
                }
            }
            #endregion


            sendBytes.AddRange(writeBytes); // 添加写入的字节

            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 4(一个寄存器2个字节,32位数字占4个字节) + 5(固定字节)
            int readCount = 8; // 读取字节的个数(因为写多线圈功能码0x0F返回的报文是8个字节，即从站地址 + 功能码 + 写入地址 + 写入数量 + CRC校验)
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            //#endregion

            //#region 3.校验
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }

            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }
            #endregion

        }
        #endregion

        #region 写单个寄存器 功能码0x06
        /// <summary>
        /// 写单个寄存器 功能码0x06
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="value">写入的值</param>
        public void WriteSingleReg(ushort startAddress, short value)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.校验
            #region 1.发送请求
            // 起始地址
            byte[] startBytes = BitConverter.GetBytes(startAddress);

            // 写入的值
            byte[] valueBytes = BitConverter.GetBytes(value); // 小端

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x06,     // 功能码
                startBytes[1], // 起始地址
                startBytes[0], // 起始地址
                valueBytes[1], // 写进去的值
                valueBytes[0]  // 写进去的值
            ];


            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);

            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 4(一个寄存器2个字节,32位数字占4个字节) + 5(固定字节)
            int readCount = 8; // 读取字节的个数(因为写多线圈功能码0x0F返回的报文是8个字节，即从站地址 + 功能码 + 写入地址 + 写入数量 + CRC校验)
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            //#endregion

            //#region 3.校验
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }

            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }
            #endregion

        }
        #endregion

        #region 写多个寄存器(32位int,4个字节需要2个寄存器) 功能码0x10
        /// <summary>
        /// 写多个寄存器(32位int,4个字节需要2个寄存器) 功能码0x10
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="values">写入的值</param>
        /// <param name="endian">字节序(大小端)</param>
        public void WriteMultipleReg32Int(ushort startAddress, int[] values, Endian endian = Endian.BigEndian)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.校验
            #region 1.发送请求
            // 起始地址
            byte[] startBytes = BitConverter.GetBytes(startAddress);

            // 数量(寄存器的数量)
            byte[] regCountBytes = BitConverter.GetBytes((ushort)values.Length * (ushort)2);

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x10,     // 功能码
                startBytes[1], // 起始地址
                startBytes[0], // 起始地址
                regCountBytes[1], // 写入数量(寄存器的数量)
                regCountBytes[0],  // 写入数量(寄存器的数量)
                (byte)(values.Length * 4), // 写入的字节数(因为一个寄存器2个字节,32位数字占4个字节，所以这里的字节数是values.Length * 4)
            ];

            foreach (int i in values)
            {
                byte[] curBytes = BitConverter.GetBytes(i); // 小端
                if (endian == Endian.BigEndian)
                {
                    curBytes = curBytes.Reverse().ToArray(); // 如果是大端则反转字节顺序
                }

                sendBytes.AddRange(curBytes); // 添加写入的值
            }


            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);


            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 4(一个寄存器2个字节,32位数字占4个字节) + 5(固定字节)
            int readCount = 8; // 读取字节的个数(因为写多线圈功能码0x0F返回的报文是8个字节，即从站地址 + 功能码 + 写入地址 + 写入数量 + CRC校验)
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            //#endregion

            //#region 3.校验
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }

            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }
            #endregion

        }
        #endregion

        #region 写多个寄存器(32位float,4个字节需要2个寄存器) 功能码0x10
        /// <summary>
        /// 写多个寄存器(32位float,4个字节需要2个寄存器) 功能码0x10
        /// </summary>
        /// <param name="startAddress">起始地址</param>
        /// <param name="values">写入的值</param>
        /// <param name="endian">字节序(大小端)</param>
        public void WriteMultipleReg32Float(ushort startAddress, float[] values, Endian endian = Endian.BigEndian)
        {
            // 1.发送请求
            // 2.获取响应数据
            // 3.校验
            #region 1.发送请求
            // 起始地址
            byte[] startBytes = BitConverter.GetBytes(startAddress);

            // 数量(寄存器的数量)
            byte[] regCountBytes = BitConverter.GetBytes((ushort)values.Length * (ushort)2);

            // 发送的bytes
            List<byte> sendBytes = [
                _slaveId,  // 从站地址
                0x10,     // 功能码
                startBytes[1], // 起始地址
                startBytes[0], // 起始地址
                regCountBytes[1], // 写入数量(寄存器的数量)
                regCountBytes[0],  // 写入数量(寄存器的数量)
                (byte)(values.Length * 4), // 写入的字节数(因为一个寄存器2个字节,32位数字占4个字节，所以这里的字节数是values.Length * 4)
            ];

            foreach (float i in values)
            {
                byte[] curBytes = BitConverter.GetBytes(i); // 小端
                if (endian == Endian.BigEndian)
                {
                    curBytes = curBytes.Reverse().ToArray(); // 如果是大端则反转字节顺序
                }

                sendBytes.AddRange(curBytes); // 添加写入的值
            }


            // CRC校验
            byte[] crcBytes = Common.CRC16(sendBytes);

            sendBytes.AddRange(crcBytes);


            _serialPort.Write(sendBytes.ToArray(), 0, sendBytes.Count);
            #endregion

            #region 2.获取响应数据
            // Thread.Sleep(30); // 等待设备响应。 因为马上读太快了,会导致读不到数据,所以这里先休眠30毫秒

            // 报文返回的有5个字节是固定的即：读取的数字的个数 * 4(一个寄存器2个字节,32位数字占4个字节) + 5(固定字节)
            int readCount = 8; // 读取字节的个数(因为写多线圈功能码0x0F返回的报文是8个字节，即从站地址 + 功能码 + 写入地址 + 写入数量 + CRC校验)
            // byte[] readBytes = new byte[readCount];
            //_serialPort.Read(readBytes, 0, readCount);

            // 也可以一个字节一个字节的读取,防止马上度太快，导致读不到数据的问题(防止丢包)
            List<byte> readBytes = new List<byte>();  // 返回的byte[]

            try
            {
                while (readBytes.Count < readCount)
                {
                    readBytes.Add((byte)_serialPort.ReadByte());
                }
            }
            catch (Exception)
            {
                // 异常处理，比如超时异常等。
                // 由于在打开串口时已经设置了ReadTimeout=500;如果报文异常，可能会抛出TimeoutException异常。
            }

            //#endregion

            //#region 3.校验
            if (readBytes[1] > 0x80) // 出异常了(协议出错)
            {
                // 这里的readBytes[2]是错误码,可以根据错误码来判断是什么错误
                throw new Exception($"出异常了,异常信息{Common.ErrorCodes[readBytes[2]]}");
            }

            List<byte> checkBytes = readBytes.GetRange(0, readBytes.Count - 2);
            byte[] checkCodeBytes = Common.CRC16(checkBytes);
            checkBytes.AddRange(checkCodeBytes);
            // SequenceEqual(你可以把它看作是两个集合的“深度比较”方法，而不是引用比较。) 是 System.Linq 命名空间下的一个扩展方法，用于比较两个集合（如 List<T>、数组等）中的元素是否完全相等
            // 元素个数相同；
            // 每个位置上的元素值都相同；
            // 顺序也完全一致。
            if (!checkBytes.SequenceEqual(readBytes))
            {
                throw new Exception("CRC校验失败，数据可能被篡改或传输错误。");
            }
            #endregion

        }
        #endregion
    }
}
